﻿using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Data.Entity;
using System.ComponentModel.DataAnnotations;

using Whoever.Framework.Struct;
using Whoever.Framework.Base;
using Whoever.Patterns.Data.Infrastructure;

namespace Whoever.Patterns.Data
{
    public class QueryFactory<TEntity> where TEntity : BusinessEntity, new()
    {
        private readonly DbContext _context;

        private readonly string _entityName;
        private readonly Type _entityType;
        private readonly object _contextBox;
        private readonly Type _contextType;
        private readonly object _dbSet;
        private readonly PropertyInfo _dbSetProperty;

        public QueryFactory(DbContext context)
        {
            _context = context;
            _dbSet = context.Set<TEntity>();

            _entityName = typeof(TEntity).Name;
            _entityType = typeof(TEntity);
            _contextBox = this.ContextBox;
            _contextType = this.ContextBox.GetType();
            _dbSetProperty = this.ContextType.GetProperty(this.EntityName);
        }

        #region PROPERTIES

        private DbContext Context
        {
            get
            {
                return _context;
            }
        }

        private object ContextBox
        {
            get
            {
                return _context;
            }
        }

        private string EntityName
        {
            get
            {
                return _entityName;
            }
        }

        private Type EntityType
        {
            get
            {
                return _entityType;
            }
        }

        private Type ContextType
        {
            get
            {
                return _contextType;
            }
        }

        private object DbSet
        {
            get
            {
                return _dbSet;
            }
        }

        private PropertyInfo DbSetProperty
        {
            get
            {
                return _dbSetProperty;
            }
        }

        #endregion PROPERTIES

        #region METHODS

        public virtual IQueryable<TEntity> GetByKeys(ViewModel<TEntity> model)
        {
            var instance = Activator.CreateInstance(model.GetType());

            foreach (var property in EntityType.GetProperties().Where(p => p.GetCustomAttributes(typeof(KeyAttribute), true).Length != 0))
            {
                if (model.GetType().GetProperty(property.Name).GetValue(model, null) == null)
                    throw new ArgumentException("All the keys must be loaded");
                var setterMethod = instance.GetType().GetProperty(property.Name).GetSetMethod();
                setterMethod.Invoke(instance, new object[] { model.GetType().GetProperty(property.Name).GetValue(model, null) });
            }

            return this.GetFiltered((ViewModel<TEntity>)instance);
        }

        public virtual IQueryable<TEntity> GetByKeys(TEntity entity)
        {
            var instance = Activator.CreateInstance(EntityType);

            foreach (var property in EntityType.GetProperties().Where(p => p.GetCustomAttributes(typeof(KeyAttribute), true).Length != 0))
            {
                if (property.GetValue(entity, null) == null)
                    throw new ArgumentException("All the keys must be loaded");
                var setterMethod = instance.GetType().GetProperty(property.Name).GetSetMethod();
                setterMethod.Invoke(instance, new object[] { entity.GetType().GetProperty(property.Name).GetValue(entity, null) });
            }

            return this.GetFiltered((TEntity)instance);
        }

        public virtual IQueryable<TEntity> GetAll()
        {
            return (IQueryable<TEntity>)DbSet;
        }

        public virtual IQueryable<TEntity> GetAll(ref Pager pager)
        {
            var result = this.GetAll();
            return PagerFactory<TEntity>.GetPaged(result, ref pager);
        }

        public virtual IQueryable<TEntity> GetAllRelated()
        {
            var virtualProperties = EntityType.GetProperties().Where(p => p.GetGetMethod().IsVirtual);
            var includeMethod = DbSetProperty.PropertyType.GetMethod("Include");
            var call = new object();

            if (virtualProperties != null && virtualProperties.Count<PropertyInfo>() > 0)
            {
                var first = true;
                foreach (var virtualProperty in virtualProperties)
                {
                    if (first)
                        call = includeMethod.Invoke(DbSet, new object[] { virtualProperty.Name.ToString() });
                    else
                        call = includeMethod.Invoke(call, new object[] { virtualProperty.Name.ToString() });
                    first = false;
                }
                return (IQueryable<TEntity>)call;
            }
            else
            {
                return (IQueryable<TEntity>)DbSet;
            }
        }

        public virtual IQueryable<TEntity> GetAllRelated(ref Pager pager)
        {
            var result = this.GetAllRelated();
            return PagerFactory<TEntity>.GetPaged(result, ref pager);
        }

        //NO FUNCIONA
        public virtual IQueryable<TEntity> GetJustRelated()
        {
            var virtualProperties = EntityType.GetProperties().Where(p => p.GetGetMethod().IsVirtual);
            var includeMethod = DbSetProperty.PropertyType.GetMethod("Include");
            var call = new object();

            if (virtualProperties != null)
            {
                var first = true;
                foreach (var virtualProperty in virtualProperties)
                {
                    if (first)
                        call = includeMethod.Invoke(DbSet, new object[] { virtualProperty.Name.ToString() });
                    else
                        call = includeMethod.Invoke(call, new object[] { virtualProperty.Name.ToString() });
                    first = false;
                }
                return (IQueryable<TEntity>)call;
            }
            else
            {
                return (IQueryable<TEntity>)DbSet;
            }
        }

        public virtual IQueryable<TEntity> GetFiltered(TEntity entity)
        {
            if (EntityType.GetProperties().Count() == 0)
                throw new Exception("Entity don't have any property");

            var query = (IQueryable<TEntity>)DbSet;
            var enumerableExtensions = typeof(System.Linq.Enumerable);
            var whereMethod = enumerableExtensions.GetMethods().Where(m => m.Name == "Where").First().MakeGenericMethod(typeof(TEntity));
            Expression whereClause = Expression.Empty();
            var expParameter = Expression.Parameter(EntityType, "e");

            var expTargetEmpty = Expression.Constant(true);
            var equalsMethodEmpty = Expression.Variable(typeof(bool), "Empty");
            var expLambda = Expression.Lambda<Func<TEntity, bool>>(equalsMethodEmpty, expParameter);

            var first = true;
            foreach (var property in EntityType.GetProperties().Where(p => !p.GetGetMethod().IsVirtual && p.GetCustomAttributes(typeof(NotMappedAttribute), true).Count() == 0))
            {
                Expression filterClause = Expression.Empty();

                var value = entity.GetType().GetProperty(property.Name).GetValue(entity, null);
                if (value != null && !string.IsNullOrEmpty(value.ToString()))
                {
                    Expression expProperty = Expression.Property(expParameter, property.Name);
                    Expression expTarget = Expression.Constant(entity.GetType().GetProperty(property.Name).GetValue(entity, null));

                    if (property.PropertyType == typeof(string))
                    {
                        expProperty = Expression.Call(expProperty, "ToLower", null);
                        expTarget = Expression.Call(expTarget, "ToLower", null);

                        var indexOfMethod = Expression.Call(expProperty, "IndexOf", null, expTarget);
                        var comparerMethod = Expression.GreaterThanOrEqual(indexOfMethod, Expression.Constant(0));

                        filterClause = comparerMethod;
                    }
                    else
                    {
                        var equalsMethod = Expression.Equal(expProperty, expTarget);

                        filterClause = equalsMethod;
                    }

                    if (first)
                    {
                        whereClause = filterClause;
                        first = false;
                    }
                    else
                    {
                        whereClause = Expression.And(whereClause, filterClause);
                    }
                }
            }

            var result = Enumerable.Empty<TEntity>();

            if (whereClause is DefaultExpression)
                result = (IEnumerable<TEntity>)DbSet;
            else
            {
                expLambda = Expression.Lambda<Func<TEntity, bool>>(whereClause, expParameter);
                result = (IEnumerable<TEntity>)whereMethod.Invoke(query, new object[] { query, expLambda.Compile() });
            }

            return result.AsQueryable<TEntity>();
        }

        public virtual IQueryable<TEntity> GetFiltered(TEntity entity, ref Pager pager)
        {
            var result = this.GetFiltered(entity);
            return PagerFactory<TEntity>.GetPaged(result, ref pager);
        }

        public virtual IQueryable<TEntity> GetFiltered(ViewModel<TEntity> model)
        {
            if (EntityType.GetProperties().Count() == 0)
                throw new Exception("Entity don't have any property");

            var query = (IQueryable<TEntity>)DbSet;
            var enumerableExtensions = typeof(System.Linq.Enumerable);
            var whereMethod = enumerableExtensions.GetMethods().Where(m => m.Name == "Where").First().MakeGenericMethod(typeof(TEntity));
            Expression whereClause = Expression.Empty();
            var expParameter = Expression.Parameter(EntityType, "e");

            var expTargetEmpty = Expression.Constant(true);
            var equalsMethodEmpty = Expression.Variable(typeof(bool), "Empty");
            var expLambda = Expression.Lambda<Func<TEntity, bool>>(equalsMethodEmpty, expParameter);

            var filterProperty = model.GetType().GetProperty("Filter");
            if (filterProperty == null) throw new Exception("View model must have Filter property");
            var filterObject = model.GetType().GetProperty("Filter").GetValue(model, null);

            var first = true;
            foreach (var property in EntityType.GetProperties().Where(p => !p.GetGetMethod().IsVirtual && p.GetCustomAttributes(typeof(NotMappedAttribute), true).Count() == 0))
                if (filterObject.GetType().GetProperty(property.Name) != null)
                {
                    Expression filterClause = Expression.Empty();
                    var value = filterObject.GetType().GetProperty(property.Name).GetValue(filterObject, null);
                    if (value != null && !string.IsNullOrEmpty(value.ToString()))
                    {
                        Expression expProperty = Expression.Property(expParameter, property.Name);
                        Expression expTarget = Expression.Constant(filterObject.GetType().GetProperty(property.Name).GetValue(filterObject, null));

                        if (property.PropertyType == typeof(string))
                        {
                            expProperty = Expression.Call(expProperty, "ToLower", null);
                            expTarget = Expression.Call(expTarget, "ToLower", null);

                            var indexOfMethod = Expression.Call(expProperty, "IndexOf", null, expTarget);
                            var comparerMethod = Expression.GreaterThanOrEqual(indexOfMethod, Expression.Constant(0));

                            filterClause = comparerMethod;
                        }
                        else
                        {
                            var equalsMethod = Expression.Equal(expProperty, expTarget);

                            filterClause = equalsMethod;
                        }

                        if (first)
                        {
                            whereClause = filterClause;
                            first = false;
                        }
                        else
                            whereClause = Expression.And(whereClause, filterClause);
                    }
                }

            var result = Enumerable.Empty<TEntity>();

            if (whereClause is DefaultExpression)
                result = (IEnumerable<TEntity>)DbSet;
            else
            {
                expLambda = Expression.Lambda<Func<TEntity, bool>>(whereClause, expParameter);
                result = (IEnumerable<TEntity>)whereMethod.Invoke(query, new object[] { query, expLambda.Compile() });
            }

            return result.AsQueryable<TEntity>();
        }

        public virtual IQueryable<TEntity> GetFiltered(ViewModel<TEntity> model, ref Pager pager)
        {
            var result = this.GetFiltered(model);
            return PagerFactory<TEntity>.GetPaged(result, ref pager);
        }

        public virtual IQueryable<TEntity> GetFiltered(Filter filter)
        {
            var entityProperties = EntityType.GetProperties();

            if (entityProperties.Count() == 0)
                throw new Exception("Entity don't have any property");

            var query = (IQueryable<TEntity>)DbSet;
            var enumerableExtensions = typeof(System.Linq.Enumerable);
            var whereMethod = enumerableExtensions.GetMethods().Where(m => m.Name == "Where").First().MakeGenericMethod(typeof(TEntity));
            Expression whereClause = Expression.Empty();
            var expParameter = Expression.Parameter(EntityType, "e");

            var equalsMethodEmpty = Expression.Variable(typeof(bool), "Empty");
            var expLambda = Expression.Lambda<Func<TEntity, bool>>(equalsMethodEmpty, expParameter);

            var filterType = filter.GetType();

            var first = true;
            foreach (var property in entityProperties.Where(p => !p.GetGetMethod().IsVirtual && p.GetCustomAttributes(typeof(NotMappedAttribute), true).Count() == 0))
                if (filterType.GetProperty(property.Name) != null)
                {
                    Expression filterClause = Expression.Empty();
                    var value = filterType.GetProperty(property.Name).GetValue(filter, null);
                    if (value != null && !string.IsNullOrEmpty(value.ToString()))
                    {
                        Expression expProperty = Expression.Property(expParameter, property.Name);
                        Expression expTarget = Expression.Constant(filterType.GetProperty(property.Name).GetValue(filter, null));

                        if (property.PropertyType == typeof(string))
                        {
                            expProperty = Expression.Call(expProperty, "ToLower", null);
                            expTarget = Expression.Call(expTarget, "ToLower", null);

                            var indexOfMethod = Expression.Call(expProperty, "IndexOf", null, expTarget);
                            var comparerMethod = Expression.GreaterThanOrEqual(indexOfMethod, Expression.Constant(0));

                            filterClause = comparerMethod;
                        }
                        else
                        {
                            var equalsMethod = Expression.Equal(expProperty, expTarget);

                            filterClause = equalsMethod;
                        }

                        if (first)
                        {
                            whereClause = filterClause;
                            first = false;
                        }
                        else
                            whereClause = Expression.And(whereClause, filterClause);
                    }
                }

            var result = Enumerable.Empty<TEntity>();

            if (whereClause is DefaultExpression)
                result = (IEnumerable<TEntity>)DbSet;
            else
            {
                expLambda = Expression.Lambda<Func<TEntity, bool>>(whereClause, expParameter);
                result = (IEnumerable<TEntity>)whereMethod.Invoke(query, new object[] { query, expLambda.Compile() });
            }

            return result.AsQueryable<TEntity>();
        }

        public virtual IQueryable<TEntity> GetFiltered(Filter filter, ref Pager pager)
        {
            var result = this.GetFiltered(filter);
            return PagerFactory<TEntity>.GetPaged(result, ref pager);
        }

        public virtual IQueryable<TEntity> GetFilteredRelated(ViewModel<TEntity> model)
        {
            var relatedQuery = GetAllRelated();

            if (EntityType.GetProperties().Count() == 0)
                throw new Exception("Entity don't have any property");

            var query = (IQueryable<TEntity>)relatedQuery;
            var enumerableExtensions = typeof(System.Linq.Enumerable);
            var whereMethod = enumerableExtensions.GetMethods().Where(m => m.Name == "Where").First().MakeGenericMethod(typeof(TEntity));
            Expression whereClause = Expression.Empty();
            var expParameter = Expression.Parameter(EntityType, "e");

            var expTargetEmpty = Expression.Constant(true);
            var equalsMethodEmpty = Expression.Variable(typeof(bool), "Empty");
            var expLambda = Expression.Lambda<Func<TEntity, bool>>(equalsMethodEmpty, expParameter);

            var first = true;
            foreach (var property in EntityType.GetProperties().Where(p => !p.GetGetMethod().IsVirtual && p.GetCustomAttributes(typeof(NotMappedAttribute), true).Count() == 0))
            {
                if (model.GetType().GetProperty(property.Name) != null)
                {
                    Expression filterClause = Expression.Empty();
                    var value = model.GetType().GetProperty(property.Name).GetValue(model, null);
                    if (value != null && !string.IsNullOrEmpty(value.ToString()))
                    {
                        Expression expProperty = Expression.Property(expParameter, property.Name);
                        Expression expTarget = Expression.Constant(model.GetType().GetProperty(property.Name).GetValue(model, null));
                        Expression equalsMethod = Expression.Equal(expProperty, expTarget);
                        filterClause = equalsMethod;
                        if (first)
                        {
                            whereClause = filterClause;
                            first = false;
                        }
                        else
                        {
                            whereClause = Expression.And(whereClause, filterClause);
                        }
                    }
                }
            }

            var result = Enumerable.Empty<TEntity>();

            if (whereClause is DefaultExpression)
                result = (IEnumerable<TEntity>)relatedQuery;
            else
            {
                expLambda = Expression.Lambda<Func<TEntity, bool>>(whereClause, expParameter);
                result = (IEnumerable<TEntity>)whereMethod.Invoke(query, new object[] { query, expLambda.Compile() });
            }

            return result.AsQueryable<TEntity>();
        }

        public virtual IQueryable<TEntity> GetFilteredRelated(ViewModel<TEntity> model, ref Pager pager)
        {
            var result = this.GetFilteredRelated(model);
            return PagerFactory<TEntity>.GetPaged(result, ref pager);
        }

        #endregion METHODS
    }
}
